Skip to main content
Jørgen Borgesen

Create a state machine in Home Assistant

I recently found my self in the need of a fairly complex automation. Being a fan of state machines I wanted to try implementing one in Home Assistant.

The problem #

We will look at a simplified version of the above problem to demonstrate how to implement a state machine in Home Assistant. At the end of the post you will find the complete problem.

At the bottom of my staircase I have a door and light that I want to turn on when the door is opened. I want the light to stay on for 2 minutes after the door is closed. To make it more complex we want to abort any timeout and keep the light on if the door is opened again before the light is turned off.

The solution: A state machine #

This diagram illustrates the behavior I want to implement: State machine

Implementation in Home Assistant #

A state machine can be in one out of a given set of states. The input select entity in Home assistant provides us with this exact functionality: It can only be in one state, and you are able to define all the possible states.

We create a new input select entity from Settings > Devices and Services > Helpers and call it Staircase State. We define our set of possible states to be INACTIVE, ACTIVE and FINISHING. We also add a new Timer entity to help us with the timeout functionality.

Creating the automation #

The state machine diagram contains all the information we need. Now we just need to translate it into a Home Assistant automation.

Setup transitions #

We want to transition between states when certain events happens. From the state diagram we see that these event triggers a state transition when

We define each of these events as triggers in our automation and give them a trigger id (in parentheses) to be able to distinguish between them in our actions.

Example: Trigger when door opens

Door opened trigger

as YAML:

type: opened
platform: device
device_id: 1698d3f9359c76ca2bca127994f252c3
entity_id: b70cbe8114e65d919e8fb97b333a8ccd
domain: binary_sensor
id: door_opened

Now that we have our triggers we can add our transition actions. We need four of them:

I found it easiest to solve this by using nested if-then actions. One outer if-then condition makes sure we are in a given state, while an inner condition is checking which trigger activated the automation. For more complex state machines this makes it easy to have multiple events trigger the same transition. Finally, the action of the inner if-then block will trigger the select service of our input select representing the current state.

Example: YAML definition for the first automation action

Transition action

as YAML:

if:
  - condition: state
    entity_id: input_select.state_staircase
    state: INACTIVE
then:
  - if:
      - condition: trigger
        id:
          - opened
    then:
      - service: input_select.select_option
        data:
          option: ACTIVE
        target:
          entity_id: input_select.state_staircase
alias: When INACTIVE

Entry and exit actions #

State machines often have actions they want to perform when entering or existing a state. In our case we have the following:

We can specify each of these as a new trigger (with a unique id) in our automation.

Example: Create a trigger for entering INACTIVE state

Entry trigger

as YAML:

platform: state
entity_id:
  - input_select.state_staircase
to: INACTIVE
id: to_inactive

Now that we have triggers for entering and exiting the states that we are interested in, we can add the actions we want in the actions part of the automation. We use an if-then action to only run an action when entering or exiting a particular state.

For instance: we want to turn the lights off when entering the INACTIVE state, meaning we make sure to only run the action if the automation was triggered by the to_inactive trigger (defined as an example trigger above). The configuration looks like this:

Entry action

as YAML:

if:
  - condition: trigger
    id:
      - to_inactive
then:
  - type: turn_off
    device_id: ...
    entity_id: ...
    domain: light
alias: Entering INACTIVE (on_entry)

Configure the automation mode #

By default automations runs in single mode, meaning that only a single instance of the automation will be running at any given time. Any new triggers will be ignored while the automation is running.

This mode will not work for our case because the entry and exist state events will be triggered when the transition actions are running. Changing the automation mode to Queued tells Home Assistant to put all trigger events in a queue and process then in sequence, running the automation once for every trigger, exactly what we want.

Extra: The complete problem #

I'll include the complete problem to show that it can't simply be solved with a simple automation with an delay that restarts every time a person is detected.

At the bottom of my staircase I have a light that I want to turn on when either the door is opened or when motion is detected in the staircase. I want the light to stay on for 2 minutes after motion is no longer detected and the door is closed. To make it more complex we want to abort any timeout and keep the light on if we detect motion or the door is opened again before the light is turned off.

The state diagram looks like this:

State machine

And the complete automation looks like this:

alias: State machine for light in stair case
description: ""
trigger:
  - type: opened
    platform: device
    device_id: 1698d3f9359c76ca2bca127994f252c3
    entity_id: b70cbe8114e65d919e8fb97b333a8ccd
    domain: binary_sensor
    id: door_opened
  - type: not_opened
    platform: device
    device_id: 1698d3f9359c76ca2bca127994f252c3
    entity_id: b70cbe8114e65d919e8fb97b333a8ccd
    domain: binary_sensor
    id: door_closed
  - type: motion
    platform: device
    device_id: af1158804ea13cc4209019680ad6725b
    entity_id: 2ca0bbfd1760298d721d4311602504b9
    domain: binary_sensor
    id: motion_detected
  - type: no_motion
    platform: device
    device_id: af1158804ea13cc4209019680ad6725b
    entity_id: 2ca0bbfd1760298d721d4311602504b9
    domain: binary_sensor
    id: motion_stopped
  - platform: event
    event_type: timer.finished
    event_data:
      entity_id: timer.timeout_light_staircase
    id: timeout
  - platform: state
    entity_id:
      - input_select.state_staircase
    to: FINISHING
    id: to_finishing
  - platform: state
    entity_id:
      - input_select.state_staircase
    id: from_finishing
    from: FINISHING
  - platform: state
    entity_id:
      - input_select.state_staircase
    to: INACTIVE
    id: to_inactive
  - platform: state
    entity_id:
      - input_select.state_staircase
    id: from_inactive
    from: INACTIVE
condition: []
action:
  - if:
      - condition: trigger
        id:
          - to_inactive
    then:
      - type: turn_off
        device_id: 1c8bb6c244b77aee0d8ddb25d1615a03
        entity_id: 83b67057766d16b88e879d32355379ab
        domain: light
    alias: Entering INACTIVE (on_entry)
  - if:
      - condition: trigger
        id:
          - from_inactive
    then:
      - type: turn_on
        device_id: 1c8bb6c244b77aee0d8ddb25d1615a03
        entity_id: 83b67057766d16b88e879d32355379ab
        domain: light
    alias: Exiting INACTIVE (on_exit)
  - if:
      - condition: trigger
        id:
          - from_finishing
    then:
      - service: timer.cancel
        data: {}
        target:
          entity_id: timer.timeout_light_staircase
    alias: Exiting FINISHING (on_exit)
  - if:
      - condition: trigger
        id:
          - to_finishing
    then:
      - service: timer.start
        data:
          duration: "00:02:00"
        target:
          entity_id: timer.timeout_light_staircase
    alias: Entering FINISHING (on_entry)
  - if:
      - condition: state
        entity_id: input_select.state_staircase
        state: INACTIVE
    then:
      - if:
          - condition: trigger
            id:
              - door_opened
              - motion_detected
        then:
          - service: input_select.select_option
            data:
              option: ACTIVE
            alias: Transition to ACTIVE
            target:
              entity_id: input_select.state_staircase
        alias: Transition to ACTIVE when motion detected or door opened
    alias: When INACTIVE
  - if:
      - condition: state
        entity_id: input_select.state_staircase
        state: ACTIVE
    then:
      - if:
          - condition: and
            conditions:
              - condition: trigger
                id:
                  - door_closed
                  - motion_stopped
              - condition: not
                conditions:
                  - condition: state
                    entity_id: >-
                      binary_sensor.smart_motion_sensor_home_security_motion_detection
                    state: "on"
                alias: Make sure there are no motion detected
              - condition: not
                conditions:
                  - condition: state
                    entity_id: binary_sensor.lumi_lumi_sensor_magnet_aq2_on_off
                    state: "on"
                alias: Make sure door is closed
        then:
          - service: input_select.select_option
            data:
              option: FINISHING
            alias: Transition to FINISHING
            target:
              entity_id: input_select.state_staircase
        alias: Transition to FINISHING when the door is closed and no motion is detected
    alias: When ACTIVE
  - if:
      - condition: state
        entity_id: input_select.state_staircase
        state: FINISHING
    then:
      - if:
          - condition: trigger
            id:
              - timeout
        then:
          - service: input_select.select_option
            data:
              option: INACTIVE
            alias: Transition to INACTIVE
            target:
              entity_id: input_select.state_staircase
        alias: Transition to INACTIVE after timer expires
      - if:
          - condition: trigger
            id:
              - door_opened
              - motion_detected
        then:
          - service: input_select.select_option
            data:
              option: ACTIVE
            target:
              entity_id: input_select.state_staircase
        alias: Transition back to ACTIVE if motion is detected or door is opened
    alias: When FINISHING
mode: queued
max: 12